import pickle

from PySide6.QtCore import QItemSelection, QModelIndex, QRegularExpression, QSortFilterProxyModel, Qt, Signal, Slot
from PySide6.QtWidgets import QAbstractItemView, QMessageBox, QTabWidget, QTableView

from adddialogwidget import AddDialogWidget
from newaddresstab import NewAddressTab
from tablemodel import TableModel

'''
地址簿-核心框架
    QTabWidget - 新建页 - 调用 NewAddressTab
               - 显示页 - 嵌入 QTableView - 绑定 QSortFilterProxyModel - 关联 QRegularExpression
    TableModel - 显示页 - https://doc.qt.io/qtforpython-6/PySide6/QtCore/QAbstractTableModel.html#PySide6.QtCore.QAbstractTableModel
    
    view/model 模式，修改model自动映射到view
'''


class AddressWidget(QTabWidget):
    """
    The central widget of the application. Most of the addressbook's functionality is contained in this class.
    """
    selection_changed = Signal(QItemSelection) # 定义signal

    def __init__(self, parent=None):
        """ Initialize the AddressWidget. """
        super().__init__(parent)

        self._table_model = TableModel()
        self._new_address_tab = NewAddressTab()
        self._new_address_tab.send_details.connect(self.add_entry)  # 响应 NewAddressTab 自定义消息（关闭 新增dialog窗口 时触发，通过消息可以隔离代码，使业务边界更清晰。否者数据处理相关的代码需要实现在 QDialog.accept 中或者 QDialog.exec 后）
        self.addTab(self._new_address_tab, "新建")

        self.setup_tabs()

    @Slot()
    def add_entry(self,menu_checked=False, name=None, address=None):
        """ Add an entry to the addressbook. """
        """" 从窗口中调用时会传入 name和address 参数 """
        """" 从菜单中调用时会传入 menu_checked 参数 """
        """ 参数 menu_checked 用于兼容从菜单QAction中调用，否则导致 name 参数接收QAction.triggered传来的checked值，原demo的代码似乎不够严谨 """
        if name is None or address is None: # 兼容从菜单调用新增窗口
            add_dialog = AddDialogWidget()
            if add_dialog.exec():
                name = add_dialog.name
                address = add_dialog.address

        if name is None or address is None: # 确保从菜单调用时正确性，原demo的代码似乎不够严谨
             return

        address = {"name": name, "address": address}
        addresses = self._table_model.addresses[:] # [:]创建副本，避免对原数据进行修改

        # The QT docs for this example state that what we're doing here
        # is checking if the entered name already exists. What they
        # (and we here) are actually doing is checking if the whole
        # name/address pair exists already - ok for the purposes of this
        # example, but obviously not how a real addressbook application
        # should behave.
        try:
            # list.remove()，list.index()都可能导致 ValueError 异常，可以使用 in 来预处理
            addresses.remove(address) # addresses 是一个copy，使用 remove 方法来实现 检查重复 的目的，为何不使用 in 运算符？
            QMessageBox.information(self, "Duplicate Name", f'The name "{name}" already exists.')
        except ValueError:
            # The address didn't already exist, so let's add it to the model.

            # Step 1: create the  row
            self._table_model.insertRows(0)

            # Step 2: get the index of the newly created row and use it to set the name.
            ix = self._table_model.index(0, 0, QModelIndex())
            self._table_model.setData(ix, address["name"], Qt.ItemDataRole.EditRole)

            # Step 3: lather, rinse, repeat for the address.
            ix = self._table_model.index(0, 1, QModelIndex())
            self._table_model.setData(ix, address["address"], Qt.ItemDataRole.EditRole)

            # Remove the newAddressTab, as we now have at least one address in the model.
            self.removeTab(self.indexOf(self._new_address_tab)) # QTabWidget.indexOf()， Returns the index position of the page occupied by the widget w, or -1 if the widget cannot be found.

            # The screenshot for the QT example shows nicely formatted
            # multiline cells, but the actual application doesn't behave
            # quite so nicely, at least on Ubuntu. Here we resize the newly
            # created row so that multiline addresses look reasonable.
            table_view = self.currentWidget() # 基于 self.removeTab 的执行，这里 self.currentWidget() 返回的是 QTableView
            table_view.resizeRowToContents(ix.row()) # Resizes the given row based on the size hints of the delegate used to render each item in the row.

    @Slot()
    def edit_entry(self):
        """ Edit an entry in the addressbook. """
        table_view = self.currentWidget()
        proxy_model = table_view.model()
        selection_model = table_view.selectionModel()

        # Get the name and address of the currently selected row.
        indexes = selection_model.selectedRows()
        if len(indexes) != 1:
            return

        # view -> model，取回底层数据
        row = proxy_model.mapToSource(indexes[0]).row()
        ix = self._table_model.index(row, 0, QModelIndex()) # QModelIndex，返回 cell 的 index
        name = self._table_model.data(ix, Qt.ItemDataRole.DisplayRole)
        ix = self._table_model.index(row, 1, QModelIndex())
        address = self._table_model.data(ix, Qt.ItemDataRole.DisplayRole)

        # Open an addDialogWidget, and only allow the user to edit the address.
        add_dialog = AddDialogWidget()
        add_dialog.setWindowTitle("Edit a Contact")

        add_dialog._name_text.setReadOnly(True)
        add_dialog._name_text.setText(name)
        add_dialog._address_text.setText(address)

        # If the address is different, add it to the model.
        if add_dialog.exec():
            new_address = add_dialog.address
            if new_address != address:
                # 写回 model
                ix = self._table_model.index(row, 1, QModelIndex())
                self._table_model.setData(ix, new_address, Qt.ItemDataRole.EditRole)

    @Slot()
    def remove_entry(self):
        """ Remove an entry from the addressbook. """
        table_view = self.currentWidget()
        proxy_model = table_view.model()
        selection_model = table_view.selectionModel()

        # Just like editEntry, but this time remove the selected row.
        indexes = selection_model.selectedRows()

        # https://doc.qt.io/qtforpython-6/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.QAbstractItemModel.removeRows
        for index in indexes:
            row = proxy_model.mapToSource(index).row()
            self._table_model.removeRows(row)

        # If we've removed the last address in the model, display the  newAddressTab
        if self._table_model.rowCount() == 0:
            self.insertTab(0, self._new_address_tab, "Address Book")

    def setup_tabs(self):
        """ Setup the various tabs in the AddressWidget. """
        groups = ["ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"]

        # 以字母分组生成 Tab 页，并以字母过滤当前 Tab 页的数据 model，以 TableView 作为显示view，并作为 Tab 页的 widget
        for group in groups:
            proxy_model = QSortFilterProxyModel(self) # 一个封装了 过滤排序 功能的代理模型，对底层的 model 进行过滤、排序
            proxy_model.setSourceModel(self._table_model) # 设置数据源
            # Whether the proxy model is dynamically sorted and filtered whenever the contents of the source model change
            proxy_model.setDynamicSortFilter(True)

            # This here be the magic: we use the group name (e.g. "ABC") to
            # build the regex for the QSortFilterProxyModel for the group's
            # tab. The regex will end up looking like "^[ABC].*", only
            # allowing this tab to display items where the name starts with
            # "A", "B", or "C". Notice that we set it to be case-insensitive.
            # 设置数据过滤、排序
            re = QRegularExpression(f"^[{group}].*")
            assert re.isValid()  # Make sure the regex is valid
            re.setPatternOptions(QRegularExpression.PatternOption.CaseInsensitiveOption)
            proxy_model.setFilterRegularExpression(re)
            proxy_model.setFilterKeyColumn(0)  # Filter on the "name" column
            proxy_model.sort(0, Qt.SortOrder.AscendingOrder)

            table_view = QTableView()
            table_view.setModel(proxy_model)
            # QTableView and QTreeView have a sortingEnabled property that controls whether the user can sort the view by clicking the view’s horizontal header.
            table_view.setSortingEnabled(True)
            # 设置选择行为
            table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
            # table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems)
            # table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectColumns)

            # 设置水平表头的最后一列自动拉伸以填满可用空间
            table_view.horizontalHeader().setStretchLastSection(True)

            # table_view.verticalHeader().hide()
            # show垂直列头，用于显示行号，数据在 QAbstractTableModel.headerData 中。注意：verticalHeader 上无法显示 行标题（例如：[行号]）
            table_view.verticalHeader().setMinimumWidth(30)
            table_view.verticalHeader().show()

            # 禁用表格编辑功能，用户无法直接在表格中编辑数据
            table_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
            table_view.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)

            # This prevents an application crash (see: https://www.qtcentre.org/threads/58874-QListView-SelectionModel-selectionChanged-Crash)  # noqa: E501
            self.viewselectionmodel = table_view.selectionModel()  # 这样做是为了保持对选择模型的引用，防止其被垃圾回收或在某些情况下被销毁
            table_view.selectionModel().selectionChanged.connect(self.selection_changed) # connect signal，这里是将Qt框架的信号 selectionChanged 转发给 selection_changed

            self.addTab(table_view, group)

    # Note: the QT example uses a QDataStream for the saving and loading.
    # Here we're using a python dictionary to store the addresses, which
    # can't be streamed using QDataStream, so we just use cpickle for this
    # example.
    # def read_from_file(self, filename):
    #     """ Read contacts in from a file. """
    #     try:
    #         f = open(filename, "rb")
    #         addresses = pickle.load(f)
    #     except IOError:
    #         QMessageBox.information(self, f"Unable to open file: {filename}")
    #     finally:
    #         f.close()
    #
    #     if len(addresses) == 0:
    #         QMessageBox.information(self, f"No contacts in file: {filename}")
    #     else:
    #         self._table_model.removeRows(0, self._table_model.rowCount())
    #         for address in addresses:
    #             # Step 1: create the  row
    #             self._table_model.insertRows(0)
    #
    #             # Step 2: get the index of the newly created row and use it to set the name.
    #             ix = self._table_model.index(0, 0, QModelIndex())
    #             self._table_model.setData(ix, address["name"], Qt.ItemDataRole.EditRole)
    #
    #             # Step 3: lather, rinse, repeat for the address.
    #             ix = self._table_model.index(0, 1, QModelIndex())
    #             self._table_model.setData(ix, address["address"], Qt.ItemDataRole.EditRole)
    #
    #             # Remove the newAddressTab, as we now have at least one address in the model.
    #             self.removeTab(self.indexOf(
    #                 self._new_address_tab))  # QTabWidget.indexOf()， Returns the index position of the page occupied by the widget w, or -1 if the widget cannot be found.
    #
    #         # The screenshot for the QT example shows nicely formatted
    #         # multiline cells, but the actual application doesn't behave
    #         # quite so nicely, at least on Ubuntu. Here we resize the newly
    #         # created row so that multiline addresses look reasonable.
    #         table_view = self.currentWidget()  # 基于 self.removeTab 的执行，这里 self.currentWidget() 返回的是 QTableView
    #         table_view.resizeRowToContents(ix.row())  # Resizes the given row based on the size hints of the delegate used to render each item in the row.

    def read_from_file(self, filename):
        """Read contacts in from a file - simplified version."""
        try:
            with open(filename, "rb") as f:
                addresses = pickle.load(f)
        except IOError:
            QMessageBox.information(self, f"Unable to open file: {filename}")
            return

        if len(addresses) == 0:
            QMessageBox.information(self, f"No contacts in file: {filename}")
            return

        # 使用新方法设置数据
        self._table_model.set_addresses(addresses)

        # 移除新建标签页（如果存在）
        if self.indexOf(self._new_address_tab) != -1:
            self.removeTab(self.indexOf(self._new_address_tab))

        # 调整行高以适应多行地址
        table_view = self.currentWidget()
        if isinstance(table_view, QTableView):
            for row in range(len(addresses)):
                table_view.resizeRowToContents(row)


    def write_to_file(self, filename):
        """ Save all contacts in the model to a file. """
        try:
            f = open(filename, "wb")
            pickle.dump(self._table_model.addresses, f)
        except IOError:
            QMessageBox.information(self, f"Unable to open file: {filename}")
        finally:
            f.close()


if __name__ == "__main__":
    import sys
    from PySide6.QtWidgets import QApplication

    app = QApplication(sys.argv)
    address_widget = AddressWidget()
    address_widget.setWindowTitle("address tab")
    address_widget.show()
    sys.exit(app.exec())
